# Copyright (c) 2009 Barry Schwartz
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

#--------------------------------------------------------------------------
import os
import subprocess
import shutil

#--------------------------------------------------------------------------

ocaml_object_extensions    = ['.cmo', '.cmx']
ocaml_library_extensions   = ['.cma', '.cmxa']
ocaml_plugin_extensions    = ['.cma', '.cmxs']

def c_object_extension(env):
    return env['OBJSUFFIX']

def c_library_extension(env):
    return env['LIBSUFFIX']

def ocaml_embedded_extensions(env):
    return [c_object_extension(env), c_library_extension(env)]

def extension_matches(file_name, *extension):
    for e in extension:
        if file_name[-len(e):] == e:
            return True
    return False

def strip_extension(file_name, *extension):
    for e in extension:
        if extension_matches(file_name, e):
            stripped_name, _ = os.path.splitext(file_name)
            return stripped_name
    return file_name

def ocamlfind(env):
    return env['OCAMLFIND']

def call_ocamlfind(env, args):
    return subprocess.call([ocamlfind(env)] + args,
                           env = os.environ)

def get_flags(env):
    return env['OCAMLFLAGS']

def get_lib_flags(env):
    return env['OCAML_LIBFLAGS']

def get_dep_flags(env):
    return env['OCAMLDEPFLAGS']

def making_native_code(env):
    return env['OCAML_COMPILER'] in ('ocamlopt', 'opt')

def get_compiler(env):
    if making_native_code(env):
        compiler = 'opt'
    else:
        compiler = 'c'
    return compiler

def get_includes(env, root_dir):
    includes = []
    for incl in env['OCAMLINCLUDES']:
        includes.append('-I')
        includes.append(os.path.join(root_dir, incl))
    return includes

def get_object_extension(env):
    if making_native_code(env):
        compiler = '.cmx'
    else:
        compiler = '.cmo'
    return compiler

def get_library_extension(env):
    if making_native_code(env):
        compiler = '.cmxa'
    else:
        compiler = '.cma'
    return compiler

def get_plugin_extension(env):
    if making_native_code(env):
        compiler = '.cmxs'
    else:
        compiler = '.cma'
    return compiler

def get_toolchain(env):
    if env['OCAML_TOOLCHAIN'] == '':
        toolchain = []
    else:
        toolchain = ['-toolchain', env['OCAML_TOOLCHAIN']]
    return toolchain

def make_easier_to_read(paths):
    # Convert to relative paths, to make SCons messages easier to
    # read.
    return [os.path.relpath(p) for p in paths]

#--------------------------------------------------------------------------

def find_dependencies(target_dir, source_path, env):
    """
    A dependency "scanner" for use within the emitter functions,
    rather than through the usual Scanner mechanism.

    This function also will copy some source files to an out-of-source
    build directory, if one is being used.
    """

    def undo_continuations(lines):
        new_lines = []
        if len(lines) != 0:
            new_lines = [lines[0]]
            for s in lines[1:]:
                if new_lines[-1][-1] == '\\':
                    new_lines[-1] = new_lines[-1][:-1] + s
                else:
                    new_lines.append(s)
        return new_lines

    def split_line(s):
        split = s.split()
        split[0] = split[0][:-1] # Remove the ':' character from the
                                 # target.
        return split

    def pick_split(splits):
        for s in splits:
            if extension_matches(s[0], '.cmi', get_object_extension(env)):
                return s
        return []

    # Be careful that we have absolute paths.
    target_dir = os.path.abspath(target_dir)
    source_path = os.path.abspath(source_path)

    source_dir = os.path.dirname(source_path)

    args = (get_toolchain(env) +
            ['dep'] +
            get_dep_flags(env) +
            get_includes(env, source_dir) +
            ['-I', source_dir, source_path])

    pipe = subprocess.Popen([ocamlfind(env)] + args,
                            stdout = subprocess.PIPE,
                            env    = os.environ)

    dep_output = pipe.communicate()[0]
    lines = undo_continuations(dep_output.splitlines())
    splits = [split_line(s) for s in lines]
    picked_split = pick_split(splits)

    rel_deps = [os.path.relpath(d, source_dir) for d in picked_split[1:]]

    deps = []
    for d in rel_deps:
        if extension_matches(d, '.ml', '.mli'):
            deps.append(os.path.join(source_dir, d))
        else:
            deps.append(os.path.join(target_dir, d))

    return deps

#--------------------------------------------------------------------------

def ocaml_object_builder(source, target, env):

    src_path = os.path.abspath(str(source[0]))
    src = os.path.basename(src_path)
    src_dir = os.path.dirname(src_path)

    target_path = os.path.abspath(str(target[0]))
    targ = os.path.basename(target_path)
    targ_dir = os.path.dirname(target_path)

    if src_dir != targ_dir:
        src_copy = os.path.join(targ_dir, src)
        shutil.copy2(src_path, src_copy)
    else:
        src_copy = src_path

    args = (get_toolchain(env) +
            [get_compiler(env)] +
            get_flags(env) +
            get_includes(env, targ_dir) +
            ['-I', targ_dir, '-c', src_copy])

    return call_ocamlfind(env, args)


def ocaml_object_emitter(target, source, env):

    t = os.path.abspath(str(target[0]))
    t = strip_extension(t, *ocaml_object_extensions)
    t_dir = os.path.dirname(t)

    s = os.path.abspath(str(source[0]))
    s = strip_extension(s, '.ml')

    new_target = [t + get_object_extension(env)]
    if making_native_code(env):
        new_target.append(t + c_object_extension(env))
    if not os.access(s + '.mli', os.R_OK):
        new_target.append(t + '.cmi')

    new_source = [s + '.ml']

    return (make_easier_to_read(new_target),
            make_easier_to_read(new_source))


def ocaml_object_scanner(node, env, path):
    ml_file = str(node)
    return find_dependencies(os.path.dirname(ml_file), ml_file, env)

#--------------------------------------------------------------------------

ocaml_interface_builder = ocaml_object_builder

def ocaml_interface_emitter(target, source, env):

    t = os.path.abspath(str(target[0]))
    t = strip_extension(t, '.cmi')
    t_dir = os.path.dirname(t)

    s = os.path.abspath(str(source[0]))
    s = strip_extension(s, '.mli')

    new_target = [t + '.cmi']
    new_source = [s + '.mli']

    return (make_easier_to_read(new_target),
            make_easier_to_read(new_source))


def ocaml_interface_scanner(node, env, path):
    mli_file = str(node)
    return find_dependencies(os.path.dirname(mli_file), mli_file, env)

#--------------------------------------------------------------------------

def ocaml_library_builder(source, target, env):

    targ = os.path.abspath(str(target[0]))
    targ_dir = os.path.dirname(targ)
    src  = [str(s) for s in source]

    args = (get_toolchain(env) +
            [get_compiler(env)] +
            get_lib_flags(env) +
            get_flags(env) +
            ['-a', '-o', targ] + src)

    return call_ocamlfind(env, args)


def ocaml_library_emitter(target, source, env):

    t = os.path.abspath(str(target[0]))
    t = strip_extension(t, *ocaml_object_extensions)
    t_dir = os.path.dirname(t)

    new_target = [t + get_library_extension(env)]

    new_source = []
    for s in source:
        s = os.path.abspath(str(s))
        s = strip_extension(s, c_object_extension(env), *ocaml_object_extensions)
        if s not in new_source:
            new_source.append(s)
    new_source = [s + get_object_extension(env) for s in new_source]

    return (make_easier_to_read(new_target),
            make_easier_to_read(new_source))

#--------------------------------------------------------------------------

def ocaml_program_builder(source, target, env):

    target_path = os.path.abspath(str(target[0]))
    target_dir  = os.path.dirname(target_path)
    src  = [str(s) for s in source]

    args = (get_toolchain(env) +
            [get_compiler(env)] +
            get_flags(env) +
            get_includes(env, target_dir) +
            ['-o', target_path] + src)

    return call_ocamlfind(env, args)


def ocaml_program_emitter(target, source, env, plugin = False):

    if plugin:
        t = strip_extension(os.path.abspath(str(target[0])), *ocaml_plugin_extensions)
        t_dir = os.path.dirname(t)
        new_target = [t + get_plugin_extension(env)]
    else:
        t = os.path.abspath(str(target[0]))
        t_dir = os.path.dirname(t)
        new_target = [t]

    new_source = []
    for s in source:
        s = os.path.abspath(str(s))

        if extension_matches(s, *ocaml_object_extensions):
            s_extless = strip_extension(s, *ocaml_object_extensions)
            if s_extless + c_object_extension(env) in new_source:
                i = 0
                while new_source[i] != s_extless + c_object_extension(env):
                    i = i + 1
                new_source[i] = s_extless + get_object_extension(env)
            elif s not in new_source:
                new_source.append(s_extless + get_object_extension(env))

        elif extension_matches(s, *ocaml_library_extensions):
            s_extless = strip_extension(s, *ocaml_library_extensions)
            if s_extless + c_library_extension(env) in new_source:
                i = 0
                while new_source[i] != s_extless + c_library_extension(env):
                    i = i + 1
                new_source[i] = s_extless + get_library_extension(env)
            elif s not in new_source:
                new_source.append(s_extless + get_library_extension(env))

        elif extension_matches(s, c_object_extension(env), c_library_extension(env)):
            s_extless = strip_extension(s, c_object_extension(env), c_library_extension(env))
            if s_extless + get_object_extension(env) not in new_source:
                new_source.append(s)

        elif not extension_matches(s, '.cmi'):
            new_source.append(s + get_object_extension(env))

    return (make_easier_to_read(new_target),
            make_easier_to_read(new_source))

#--------------------------------------------------------------------------

def ocaml_plugin_builder(source, target, env):

    targ = str(target[0])
    src  = [str(s) for s in source]

    args = (get_toolchain(env) +
            [get_compiler(env)] +
            get_flags(env) +
            ['-shared', '-o', targ] + src)

    return call_ocamlfind(env, args)


def ocaml_plugin_emitter(target, source, env):
    return ocaml_program_emitter(target, source, env, plugin = True)

#--------------------------------------------------------------------------

def ocaml_embedded_builder(source, target, env):

    targ = str(target[0])
    src  = [str(s) for s in source]

    args = (get_toolchain(env) +
            [get_compiler(env)] +
            get_flags(env) +
            ['-output-obj', '-o', targ] + src)

    return call_ocamlfind(env, args)


def ocaml_embedded_emitter(target, source, env):
    return ocaml_program_emitter(target, source, env)

#--------------------------------------------------------------------------

def generate(env):

    env['BUILDERS']['OCamlObject'] =  env.Builder(action = ocaml_object_builder,
                                                  emitter = ocaml_object_emitter,
                                                  source_scanner = env.Scanner(function = ocaml_object_scanner,
                                                                               skeys = ['.ml']))

    env['BUILDERS']['OCamlInterface'] = env.Builder(action = ocaml_interface_builder,
                                                    emitter = ocaml_interface_emitter,
                                                    source_scanner = env.Scanner(function = ocaml_interface_scanner,
                                                                                 skeys = ['.mli']))
 
    env['BUILDERS']['OCamlLibrary'] = env.Builder(action = ocaml_library_builder,
                                                  emitter = ocaml_library_emitter,
                                                  src_suffix = ['.cmo', '.cmx'])

    env['BUILDERS']['OCamlPlugin'] = env.Builder(action = ocaml_plugin_builder,
                                                  src_suffix = ['.cmx', '.cmo',
                                                                c_object_extension(env),
                                                                '.cmxa', '.cma'],
                                                 emitter = ocaml_plugin_emitter)

    env['BUILDERS']['OCamlProgram'] = env.Builder(action = ocaml_program_builder,
                                                  src_suffix = ['.cmx', '.cmo',
                                                                c_object_extension(env),
                                                                '.cmxa', '.cma'],
                                                  emitter = ocaml_program_emitter)

    env['BUILDERS']['OCamlEmbedded'] = env.Builder(action = ocaml_embedded_builder,
                                                   src_suffix = ['.cmx', '.cmo',
                                                                 c_object_extension(env),
                                                                 '.cmxa', '.cma'],
                                                   emitter = ocaml_embedded_emitter)

    
    env['OCAMLFIND']       = 'ocamlfind'
    env['OCAML_TOOLCHAIN'] = ''

    env['OCAMLDSORT']      = 'ocamldsort'
    env['OCAMLDSORTFLAGS'] = []

    # The compiler: 'opt', 'ocamlopt', 'c', 'ocamlc', etc.
    env['OCAML_COMPILER'] = 'opt'

    # Specify flags as Python sequences.
    env['OCAMLFLAGS']     = ['-g']
    env['OCAML_LIBFLAGS'] = []
    env['OCAMLDEPFLAGS']  = []

    env['OCAMLINCLUDES']  = []


def exists(env):
        return env.Detect('ocaml')

#--------------------------------------------------------------------------

# local-variables:
# python-indent: 4
# end:
